package com.csaba.connector.web.util;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import net.htmlparser.jericho.Source;
import net.htmlparser.jericho.TextExtractor;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import com.csaba.connector.ServiceException;
import com.csaba.connector.model.Session;
import com.csaba.util.LoggerHelper;

/**
 * This class provides helper methods for HTML based internet banking implementations. The methods provides
 * an extra layer above the Apache HTTP Client by adding certain extra functionalities, such as:
 * <ul>
 * <li>logging and debugging</li>
 * <li>handling for HTTP redirects (302)</li>
 * <li>using modified host configuration to support untrusted certs (such as NetLock based certs)</li>
 * <li>session handling based on session cookie and URL rewritting as well</li>
 * </ul>
 * 
 * @author gyenesge
 *
 */
@SuppressWarnings( "serial" )
public class WebSession extends Session
{
	private final static String JSESSIONID = ";jsessionid=";

	private static final Logger logger = LoggerHelper.getDefaultLogger();

	protected DefaultHttpClient client;
	private HttpHost host;
	private ClientConnectionManager connectionManager;
	private boolean urlRewriteEnabled = false;
	private boolean forceCookieSession = false;
	private String userAgent;

	private String debugFilePath;
	private boolean dumpText = false;

	private boolean debugCookies;
	protected Charset preferredEncoding = Charset.forName("UTF8");

	private RedirectHandler redirectHandler;

	public WebSession()
	{
		super();
		final String defaultUserAgent = System.getProperty("user.agent");
		if ( defaultUserAgent != null && defaultUserAgent.length() > 0 )
			this.userAgent = defaultUserAgent;
	}

	public String doPost( String url, final NameValuePair[] params ) throws ServiceException
	{
		url = appendSessionId(url);

		final List<NameValuePair> formparams = new ArrayList<NameValuePair>();
		for ( int i = 0; i < params.length; i++ )
		{
			formparams.add(params[i]);
		}

		UrlEncodedFormEntity entity;
		try
		{
			entity = new UrlEncodedFormEntity(formparams, "UTF-8");
		}
		catch ( final UnsupportedEncodingException e )
		{
			throw new ServiceException("Failed to convert POST parameters.", e);
		}

		final HttpPost method = new HttpPost(url);
		method.setEntity(entity);

		logger.fine("POST: " + url);
		final String result = execute(method);
		dumpText(result);
		return result;
	}

	public String doGet( String url ) throws ServiceException
	{
		url = appendSessionId(url);

		final HttpGet method = new HttpGet(url);
		logger.fine("GET: " + url);
		final String result = execute(method);
		dumpText(result);
		return result;
	}

	protected DefaultHttpClient initHttpClient()
	{
		if ( connectionManager != null )
		{
			return new DefaultHttpClient(connectionManager, new BasicHttpParams());
		}
		else
		{
			return new DefaultHttpClient();
		}
	}

	protected String execute( final HttpRequestBase request ) throws ServiceException
	{
		// lazy initialization
		if ( client == null )
		{
			client = initHttpClient();

			//Provide custom retry handler is necessary
			client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(3, false));

			//set up GZIP for HTTP communication
			client.addRequestInterceptor(new HttpRequestInterceptor()
			{

				@Override
				public void process( final HttpRequest request, final HttpContext context ) throws HttpException,
						IOException
				{
					if ( !request.containsHeader("Accept-Encoding") )
					{
						request.addHeader("Accept-Encoding", "gzip");
					}
				}

			});

			client.addResponseInterceptor(new HttpResponseInterceptor()
			{

				@Override
				public void process( final HttpResponse response, final HttpContext context ) throws HttpException,
						IOException
				{
					final HttpEntity entity = response.getEntity();
					final Header ceheader = entity.getContentEncoding();
					if ( ceheader != null )
					{
						final HeaderElement[] codecs = ceheader.getElements();
						for ( int i = 0; i < codecs.length; i++ )
						{
							if ( codecs[i].getName().equalsIgnoreCase("gzip") )
							{
								response.setEntity(new GzipDecompressingEntity(response.getEntity()));
								return;
							}
						}
					}
				}

			});

		}

		HttpEntity entity = null;

		try
		{
			if ( userAgent != null )
				request.setHeader("User-Agent", userAgent);

			HttpResponse response = null;
			if ( host == null )
			{
				response = client.execute(request);
			}
			else
			{
				response = client.execute(host, request);
			}

			detectSessionCookie();
			debugCookies();

			final int statusCode = response.getStatusLine().getStatusCode();
			entity = response.getEntity();

			if ( statusCode == HttpStatus.SC_OK )
			{
				if ( entity != null )
				{
					return EntityUtils.toString(entity);
				}
				return "";
			}
			else if ( statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_MOVED_PERMANENTLY )
			{
				final Header locationHeader = response.getFirstHeader("location");
				if ( locationHeader != null )
				{
					String redirectLocation = locationHeader.getValue();
					detectSessionId(redirectLocation);
					if ( redirectHandler != null )
						redirectLocation = redirectHandler.redirectedTo(redirectLocation);

					//close connection before starting a new one. 
					if ( entity != null )
						entity.consumeContent();

					logger.fine("Being redirected to " + redirectLocation);
					return doGet(redirectLocation);
				}
				else
				{
					throw new ServiceException("Invalid redirect.", "Missing location.");
				}
			}
			else
			{
				throw new ServiceException("Unexpected HTTP response:" + statusCode, response.getStatusLine()
						.toString());
			}

		}
		catch ( final Exception e )
		{
			throw new ServiceException("Unexpected exception in HTTP communication.", e);
		}
		finally
		{
			// Release the connection.
			if ( entity != null )
				try
				{
					entity.consumeContent();
				}
				catch ( final IOException e )
				{
					logger.log(Level.SEVERE, "Failed to close connection correctly.", e);
				}
		}
	}

	protected void detectSessionCookie()
	{
		final List<Cookie> cookies = client.getCookieStore().getCookies();
		for ( final Cookie cookie : cookies )
		{
			if ( cookie.getName().equals("JSESSIONID") )
			{
				final String sessionId = cookie.getValue();
				if ( getSessionId() != null && !getSessionId().equals(sessionId) )
				{
					logger.severe("Session ID is changed from " + getSessionId() + " to " + sessionId);
				}
				setSessionId(sessionId);
			}
		}
	}

	/**
	 * Appends session ID to the URL, if session ID is already set, and the URL is not yet containing session ID.
	 * @param url URL to check and be appended.
	 * @return the URL with the session ID set.
	 */
	private String appendSessionId( final String url )
	{

		final boolean hasSessionId = url.contains(JSESSIONID);
		final boolean sessionIdSetInSession = getSessionId() != null;
		if ( hasSessionId || !sessionIdSetInSession )
		{
			if ( hasSessionId )
			{
				detectSessionId(url);
			}
			return url;
		}

		if ( !isUrlRewriteEnabled() || isForceCookieSession() )
			return url;

		final StringBuilder result = new StringBuilder();
		final int queryPos = url.indexOf('?');
		if ( queryPos > -1 )
		{
			//query sting is available
			result.append(url.substring(0, queryPos));
			result.append(JSESSIONID);
			result.append(getSessionId());
			//result.append(';');
			result.append(url.substring(queryPos));
		}
		else
		{//noquery
			result.append(url);
			result.append(JSESSIONID);
			result.append(getSessionId());
			//result.append(';');
		}
		final String stringResult = result.toString();
		logger.fine("URL with session ID: " + stringResult);
		return stringResult;
	}

	public void debugFile( String fileName, final String content )
	{
		logger.fine(fileName + " result is received. Length: " + content.length());
		if ( getDebugFilePath() != null )
		{
			try
			{
				if ( fileName.indexOf(".") < 0 )
					fileName = fileName + ".txt";
				final FileOutputStream fos = new FileOutputStream(getDebugFilePath() + fileName);
				fos.write(content.getBytes(preferredEncoding));
				fos.flush();
				fos.close();
			}
			catch ( final Exception ex )
			{
				logger.log(Level.SEVERE, "Error creating debug file.", ex);
			}
		}

	}

	public void dumpText( final String content )
	{
		if ( dumpText )
		{
			try
			{
				final Source source = new Source(new StringReader(content));
				final String extracted = new TextExtractor(source).toString();
				final int count = extracted.length() / 80 + 1;
				for ( int i = 0; i < count; i++ )
				{
					final int start = i * 80;
					final int stop = start + 80;
					if ( stop > extracted.length() )
					{
						logger.fine(extracted.substring(start));
					}
					else
					{
						logger.fine(extracted.substring(start, stop));
					}
				}
			}
			catch ( final IOException e )
			{
				logger.log(Level.SEVERE, "Failed to dump text to log.", e);
			}
		}

	}

	private void debugCookies()
	{
		if ( isDebugCookies() )
		{
			// Get all the cookies
			final List<Cookie> cookies = client.getCookieStore().getCookies();
			// Display the cookies
			logger.fine("Present cookies: ");
			for ( final Cookie cookie : cookies )
			{
				logger.fine(" - " + cookie.toString());
			}
		}
	}

	public void setDebugFilePath( final String debugFilePath )
	{
		this.debugFilePath = debugFilePath;
	}

	public String getDebugFilePath()
	{
		return debugFilePath;
	}

	@Override
	public void invalidateSession() throws ServiceException
	{
		super.invalidateSession();

		client = null;
	}

	/**
	 * Otp is using URL rewriting for session tracking. This method detects and store session ID if it is in the the received URL.
	 * @param url URL containing session ID. Maybe...
	 */
	private void detectSessionId( final String url )
	{
		int pos = url.indexOf(JSESSIONID);
		if ( pos > 0 )
		{
			setUrlRewriteEnabled(true);
			String sessionId = url.substring(pos + JSESSIONID.length());
			pos = sessionId.indexOf('?'); //start of the querystring 
			if ( pos >= 0 )
			{
				sessionId = sessionId.substring(0, pos);
			}
			if ( getSessionId() != null && !getSessionId().equals(sessionId) )
			{
				logger.severe("Session ID is changed from " + getSessionId() + " to " + sessionId);
			}
			setSessionId(sessionId);
			logger.fine("URL rewritting session identified: " + sessionId);
		}
	}

	public void setHost( final HttpHost host )
	{
		this.host = host;
	}

	public HttpHost getHost()
	{
		return host;
	}

	public void setConnectionManager( final ClientConnectionManager connectionManager )
	{
		this.connectionManager = connectionManager;
	}

	public ClientConnectionManager getConnectionManager()
	{
		return connectionManager;
	}

	public void setUrlRewriteEnabled( final boolean urlRewriteEnabled )
	{
		this.urlRewriteEnabled = urlRewriteEnabled;
	}

	public boolean isUrlRewriteEnabled()
	{
		return urlRewriteEnabled;
	}

	public void setUserAgent( final String userAgent )
	{
		this.userAgent = userAgent;
	}

	public String getUserAgent()
	{
		return userAgent;
	}

	public void setDebugCookies( final boolean debugCookies )
	{
		this.debugCookies = debugCookies;
	}

	public boolean isDebugCookies()
	{
		return debugCookies;
	}

	public void setRedirectHandler( final RedirectHandler redirectHandler )
	{
		this.redirectHandler = redirectHandler;
	}

	public RedirectHandler getRedirectHandler()
	{
		return redirectHandler;
	}

	public interface RedirectHandler
	{
		String redirectedTo( String url );
	}

	public boolean isDumpText()
	{
		return dumpText;
	}

	public void setDumpText( final boolean dumpText )
	{
		this.dumpText = dumpText;
	}

	public void setForceCookieSession( final boolean forceCookieSession )
	{
		this.forceCookieSession = forceCookieSession;
	}

	public boolean isForceCookieSession()
	{
		return forceCookieSession;
	}

	static class GzipDecompressingEntity extends HttpEntityWrapper
	{

		public GzipDecompressingEntity( final HttpEntity entity )
		{
			super(entity);
		}

		@Override
		public InputStream getContent() throws IOException, IllegalStateException
		{

			// the wrapped entity's getContent() decides about repeatability
			final InputStream wrappedin = wrappedEntity.getContent();

			return new GZIPInputStream(wrappedin);
		}

		@Override
		public long getContentLength()
		{
			// length of ungzipped content is not known
			return -1;
		}

	}

}